package com.micro.service.mongoDao;

import com.micro.service.annotation.QueryField;
import com.mongodb.client.result.DeleteResult;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.data.util.Pair;
import org.springframework.util.Assert;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.List;

/**
 * Create by Vic Xu on 2019/3/9
 */

public abstract class BaseMongoDaoImpl<T> implements BaseMongoDao<T> {

    @Autowired
    private MongoTemplate mongoTemplate;

    public void insert(T t) {
        mongoTemplate.insert(t);
    }

    public DeleteResult delete(T t) {
        return mongoTemplate.remove(t);
    }

    @Override
    public DeleteResult deleteById(Object id) {
        Criteria criteria = Criteria.where("id").is(id);
        Query query = new Query(criteria);
        mongoTemplate.findAndRemove(query, getObjectClass());
        return null;
    }

    @Override
    public DeleteResult deleteByCondition(T t) {
        Query query = buildBaseQuery(t);
        return mongoTemplate.remove(query, getObjectClass());
    }


    @Override
    public DeleteResult delete(Query query) {
        return mongoTemplate.remove(query, getObjectClass());
    }

    @Override
    public void update(T t) {
        mongoTemplate.save(t);
    }

    @Override
    public void update(Query query, Update update) {
        mongoTemplate.updateMulti(query, update,getObjectClass());
    }

    @Override
    public T findById(Object id) {
        return mongoTemplate.findById(id, getObjectClass());
    }

    @Override
    public List<T> findAll() {
        return mongoTemplate.findAll(getObjectClass());
    }

    @Override
    public List<T> find(Query query) {
        return mongoTemplate.find(query, getObjectClass());
    }

    @Override
    public List<T> findByCondition(T t) {
        Query query = buildBaseQuery(t);
        return mongoTemplate.find(query, getObjectClass());
    }

    @Override
    public T findById(String id) {
        return null;
    }

    @Override
    public boolean exists(Query query) {
        return mongoTemplate.exists(query, getObjectClass());
    }

    @Override
    public boolean exists(T t) {
        return exists(getIdQueryFor(t));
    }

    @Override
    public boolean exitsById(Object id) {
        return exists(findById(id));
    }

    @Override
    public long count(Query query) {
        return mongoTemplate.count(query, getObjectClass());
    }

    private Class<T> getObjectClass() {
        Class<T> tClass = (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        return tClass;
    }

    private Query getIdQueryFor(Object object) {
        Pair<String, Object> id = this.extractIdPropertyAndValue(object);
        return new Query(Criteria.where((String) id.getFirst()).is(id.getSecond()));
    }

    private Pair<String, Object> extractIdPropertyAndValue(Object object) {
        Assert.notNull(object, "Id cannot be extracted from 'null'.");
        Class<?> objectType = object.getClass();
        if (object instanceof Document) {
            return Pair.of("_id", ((Document) object).get("_id"));
        } else {
            MongoPersistentEntity<?> entity = mongoTemplate.getConverter().getMappingContext().getPersistentEntity(objectType);
            if (entity != null && entity.hasIdProperty()) {
                MongoPersistentProperty idProperty = entity.getIdProperty();
                return Pair.of(idProperty.getFieldName(), entity.getPropertyAccessor(object).getProperty(idProperty));
            } else {
                throw new MappingException("No id property found for object of type " + objectType);
            }
        }
    }

    private Query buildBaseQuery(T t) {
        Query query = new Query();
        Field[] fields = t.getClass().getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            try {
                Object o = field.get(t);
                if (o != null) {
                    QueryField queryField = field.getAnnotation(QueryField.class);
                    if (queryField != null)
                        query.addCriteria(queryField.type().buildCriteria(queryField, field, o));
                }
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
        return query;
    }

}
